# Spring MVC 和 Struts2 对比

  • Spring MVC 的入口是 Servlet,而 Struts2 是 Filter

  • Spring MVC 会稍微比 Struts2 快些:Spring MVC 是基于方法设计,而 Sturts2 是基于类,每次发一次请求都会实例一个 Action

  • Spring MVC 使用更加简洁,开发效率 Spring MVC 比 struts2 高(支持 JSR303,处理 Ajax 请求更方便)

  • Struts2 的 OGNL 表达式使页面的开发效率相比 Spring MVC 更高些

  • https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html

    Spring_MVC_Table_of_Contents

# Spring MVC 执行流程

  • 在 Spring MVC 框架中,控制器实际上由两个部分共同组成,即拦截所有用户请求和处理请求的通用代码都由前端控制器 DispatcherServlet 完成,而实际的业务控制(诸如调用后台业务逻辑代码,返回处理结果等)则由 Controller 处理
  • org.springframework.web.servlet.DispatcherServlet#doDispatch

Spring_MVC请求——响应的完整流程

  1. 客户端向服务器发送请求,如果匹配 DispatcherServlet 的请求映射路径(在 web.xml 中指定),则 Web 容器将该请求转交给 DispatcherServlet 处理
  2. DispatcherServlet 根据请求的信息(URL、HTTP 方法等),调用 HandlerMapping 找到处理请求的 handler 对象(处理器包含了控制器方法的逻辑)以及 handler 对象对应的 interceptor,这些对象会被封装到一个 HandlerExecutionChain(执行链) 对象当中返回给 DispatcherServlet
  3. DispatcherServlet 根据获得的 handler,选择一个合适的 HandlerAdapter,以统一的接口对 handler 方法进行调用完成实际处理请求(即通过 HandlerAdapter 运行 HandlerExecutionChain 对象)。在提取请求中的模型数据,填充 handler 的入参过程中,根据配置,Spring 还会完成一些额外的工作:
    • 消息转换:将请求消息(如 Json、xml 等数据)转换成一个对象,将对象转换为指定的响应信息
    • 数据转换:对请求消息进行数据转换,如 String 转换成 Integer、Double 等
    • 数据格式化:对请求消息进行数据格式化,如将字符串转换成格式化数字或格式化日期等
    • 数据验证:验证数据的有效性(长度、格式等),验证结果存储到 BindingResult 或 Errors 中
  4. handler 执行完成后,向 DispatcherServlet 返回一个 ModelAndView 对象,ModelAndView 对象包含视图逻辑名和模型数据信息
  5. DispatcherServlet 根据返回的 ModelAndView,选择一个合适的 ViewResolver(视图解析器)完成逻辑视图名到真实视图对象的解析,得到真实的视图对象 View
  6. DispatcherServlet 使用得到的 View 对象对 ModelAndView 中的模型数据进行视图渲染
  7. DispatcherServlet 将视图渲染结果返回给客户端

Spring MVC 处理请求并非一定需要经过全流程

// DispatcherServlet 中的初始化策略方法
// 默认使用“DispatcherServlet.properties”文件(与 DispatcherServlet 类在同一个包中)来确定各组件的实现类
protected void initStrategies(ApplicationContext context) {
    // 用于处理上传请求。处理方法是将普通的 request 包装成 MultipartttpservletRequest,后者可以直接调用 getFile 方法获取
    initMultipartResolver(context);
    // SpringMVC 主要有两个地方用到了 Locale:一是 ViewResolver 视图解析的时候;二是用到国际化资源或者主题的时候
    initLocaleResolver(context);
    // 用于解析主题
    // SpringMVC 中一个主题对应一个 properties 文件,里面存放着跟当前主题相关的所有资源,如图片、css 样式等。SpringMVC 的主题也支持国际化
    initThemeResolver(context);
    // 用来查找 Handler
    initHandlerMappings(context);
    // 从名字上看,它就是一个适配器。servlet需要的处理方法的结构却是固定的,都是以 request 和 response 为参数的方法。
    // 如何让固定的 servlet 处理方法调用灵活的 Handler 来进行处理?这就是 HandlerAdapter 要做的事情
    initHandlerAdapters(context);
    // 对异常情况进行处理
    initHandlerExceptionResolvers(context);
    // 有的 Handler 处理完后并没有设置 View 也没有设置 Viewlame,这时就需要从 request 获取ViewName 了,
    // 如何从 request 中获取 ViewName 就是 RequestToViewNameTranslator 要做的事情了。
    initRequestToViewNameTranslator(context);
    // ViewResolver 用来将 String 类型的视图名和 Locale 解析为 View 类型的视图
    // View 是用来渲染页面的,也就是将程序返回的参数填入模板里,生成 html(也可能是其它类型)文件
    initViewResolvers(context);
    // 用来管理 FlashMap 的,FlashMap 主要用在 redirect 重定向中传递参数
    initFlashMapManager(context);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 常用 API

  • Controller 接口
    ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response)

  • Mode 接口,模型数据的存储容器,类似 Map 接口
    Model addAttribute(String attributeName, Object attributeValue):// 添加模型数据

  • ModelAndView 类
    ModelAndView addObject(String attributeName, Object attributeValue): 添加模型数据
    void setViewName(String viewName):设置逻辑视图名,视图解析器会根据该名字解析到具体的视图页面

// 在 Bean 中获取 request、session
@Autowired
private HttpServletRequest request; // 注入的 request 为代理对象

// 非表现层获取 request、session 的方法
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpSession session = attrs.getRequest().getSession();

// interceptor 获取当前被拦截的请求处理方法
// 动态的请求:handler -----> HandlerMethod 对象
// 静态的请求:handler -----> DefalueMethod 对象 <mvc:default-handler>
HandlerMethod hm = (HandlerMethod) handler;
Method method = hm.getMethod();
1
2
3
4
5
6
7
8
9
10
11
12
13

只有无状态的 Bean 才可以在多线程环境下共享,在 Spring 中,绝大部分 Bean 都可以声明为 singleton 作用域
Spring 对一些 Bean(如 RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder 等)中非线程安全的“状态性对象”采用 ThreadLocal 进行封装,让它们也成为线程安全的“状态性对象”,因此,有状态的 Bean 就能够以 singleton 的方式在多线程中正常工作

通过 @Autowired 方式依赖注入得到 HttpServletRequest、HttpServletResponse、HttpSession、WebRequest 是线程安全的:
在 Spring 解析 HttpServletRequest 类型的 @Autowired 依赖注入时,注入的是 JDK 动态代理对象
该代理对象的处理器是:AutowireUtils.ObjectFactoryDelegatingInvocationHandler,内部实际实例由 WebApplicationContextUtils.RequestObjectFactory 动态提供,数据由 RequestContextHolder 请求上下文提供,请求上下文的数据在请求达到时被赋值(FrameworkServlet#initContextHolders)

对于实体 Bean 在多线程中的处理:1. 对于实体 Bean 一般通过方法参数的的形式传递(参数是局部变量,所以多线程之间不会有影响);2. 有的地方对于有状态的 Bean 直接使用 prototype 原型模式来进行解决;3. 对于使用 Bean 的地方可以通过 new 的方式来创建)

# 请求处理方法

# 请求处理方法可返回的类型 (opens new window)

  • ModelAndView、Model、Map、View、String、void、对象类型、StreamingResponseBody
  • HttpEntity<T>、ResponseEntity<T>
  • 若方法的返回值为 String
    1. 此 String 表示逻辑视图名称(即用于 ViewResolver 解析的视图名),完整物理的视图名是:前缀+逻辑视图名+后缀
    2. 若 String 中包含 “forward:” 前缀,表示请求转发,其后的字符作为 URL 处理,相当于 request.getRequestDispatcher("").forward(request,response);
    3. 若 String 中包含 “redirect:” 前缀,表示重定向,其后的字符作为 URL 处理,相当于 response.sendRedirect("");
  • 若没有返回逻辑视图名称,则默认使用被访问的 RequestMapping 的 value 值作为逻辑视图名称

# 请求处理方法可出现的参数类型 (opens new window)

  • Spring MVC 会根据请求方法签名不同,将请求消息中的信息以一定的方式转换并绑定到请求方法的参数中
  • HttpServletRequest、HttpServletResponse、HttpSession、HttpEntity<T>
  • InputStream、OutputStream
  • Map、ModelMap、Model
  • MultipartFile、Part
  • 简单类型:参数为基本数据类型时,必须保证请求参数的值不能为 null 或 "",否则会出现数据转换的异常;参数为包装类型或 String 类型时,请求参数的值可以为 null 或 ""
  • 复合类型:请求参数为 形参名.属性;属性类型为 List 时,需要在请求参数中指定 List 的下标
  • 数组、集合:有多个同名的请求参数时,Spring MVC 会自动封装成数组,或者在传递数组参数时每个参数的数组元素通过逗号分隔(注意:集合类型参数必须标注 @RequestParam
  • Data
  • Optional:可以区分没传值还是传了 null

# 视图和视图解析器 (opens new window)

  • 视图接口 org.springframework.web.servlet.View
  • 逻辑视图需要经由视图解析器(ViewResolver)的定位后,才能找到视图将数据模型进行渲染;非逻辑视图则并不需要进一步定位视图的位置,它只需要直接将数据模型渲染出来即可

Spring_MVC常用视图关系模型

  • 常见的视图解析器:InternalResourceViewResolver、FreeMarkerViewResolver、ThymeleafViewResolver、BeanNameViewResolver、ContentNegotiatingViewResolver、HandlerExceptionResolver

# 配置 DispatcherServlet

# 使用 web.xml 配置

  • 配置 DispatcherServlet 作为前端控制器来拦截用户请求,修改 contextConfigLocation 的参数值为 Spring MVC 的配置文件路径(classpath:mvc.xml),并设置为 load-on-startup(启动时创建创建所有处理器)
  • url-pattem 元素的值使用 "*.do"(若使用"/",还需在 mvc.xml 中配置 <mvc:default-servlet-handler />,对进入 DispatcherServlet 的 URL 进行检查,如果发现是静态资源的请求,就将该请求转由 Web 应用服务器默认的 Servlet 处理;如果不是静态资源的请求,则由 DispatcherServlet 继续处理)
  • 配置字符编码过滤器:使用 CharacterEncodingFilter 作为过滤器,修改 encoding 的参数值为 UTF-8,forceEncoding 的参数值为 true

先执行 ContextLoaderListener#contextInitialized 启动 Spring 容器,再初始化 DispatcherServlet 来启动 web 容器

<web-app>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/root-context.xml</param-value>
    </context-param>

    <servlet>
        <servlet-name>app1</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/app1-context.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>app1</servlet-name>
        <url-pattern>/app1/*</url-pattern>
    </servlet-mapping>

</web-app>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 使用 Java Config 配置

在 Servlet 3.1 规范之后已经允许 Web 容器不通过 web.xml 配置,只需要实现 ServletContainerInitializer 接口即可。在 Spring MVC 中已经提供了 ServletContainerInitializer 的实现类 SpringServletContainerInitializer,这个实现类会遍历 WebApplicationInitializer 接口的实现类,加载其所配置的内容。

  • 方式 1:实现 WebApplicationInitializer

    public class MyWebApplicationInitializer implements WebApplicationInitializer {
    
        @Override
        public void onStartup(ServletContext servletCxt) {
            // Load Spring web application configuration
            AnnotationConfigWebApplicationContext ac = new AnnotationConfigWebApplicationContext();
            appContext.register(MyWebConfig.class);
            appContext.refresh();
    
            // Create and register the DispatcherServlet
            DispatcherServlet servlet = new DispatcherServlet(appContext);
            ServletRegistration.Dynamic registration = servletCxt.addServlet("app", servlet);
            registration.setLoadOnStartup(1);
            registration.addMapping("/app/*");
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class MyWebApplicationInitializer implements WebApplicationInitializer {
    
        @Override
        public void onStartup(ServletContext container) {
            XmlWebApplicationContext appContext = new XmlWebApplicationContext();
            appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
    
            ServletRegistration.Dynamic registration = container.addServlet("dispatcher", new DispatcherServlet(appContext));
            registration.setLoadOnStartup(1);
            registration.addMapping("/");
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
  • 方式 2:继承 AbstractAnnotationConfigDispatcherServletInitializer 或 AbstractDispatcherServletInitializer

    // 配置 WebApplicationContext 层级
    // 如果不需要应用上下文分层,可以通过 getRootConfigClasses() 方法返回所有配置,而 getServletConfigClasses() 方法返回 null
    public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    
        @Override
        protected Class<?>[] getRootConfigClasses() {
            return new Class<?>[] { RootConfig.class };
        }
    
        @Override
        protected Class<?>[] getServletConfigClasses() {
            return new Class<?>[] { MyWebConfig.class };
        }
    
        @Override
        protected String[] getServletMappings() {
            return new String[] { "/" };
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {
    
        @Override
        protected WebApplicationContext createRootApplicationContext() {
            return null;
        }
    
        @Override
        protected WebApplicationContext createServletApplicationContext() {
            XmlWebApplicationContext cxt = new XmlWebApplicationContext();
            cxt.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
            return cxt;
        }
    
        @Override
        protected String[] getServletMappings() {
            return new String[] { "/" };
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19

# 配置 Spring MVC

# 使用 XML 配置 —— mvc.xml

  • 配置 3 个 Spring MVC 核心组件 Bean(Spring4.0 之后可以不配置,使用默认的),包括 BeanNameUrlHandlerMapping(处理器转换器)、SimpleControllerHandlerAdapter(处理器适配器)、InternalResourceViewResolver(视图解析器,主要通过设置前缀、后缀,以及控制器中方法来返回视图名的字符串,以得到实际页面)
  • 定义处理用户请求的处理器 Bean:id 或 name(要有斜杠)、class
  • 修改视图解析器
    在 mvc.xml 中重新配置视图解析器 InternalResourceViewResolver,修改属性 prefix、suffix(默认为空字符串)

# 使用 Java Config 配置 (opens new window)

  • extends WebMvcConfigurerAdapter 或 implements WebMvcConfigurer

# Spring MVC 的常用注解

  • 在 mvc.xml 中开启 Spring MVC 注解的解析器 <mvc:annotation-driven/>,使用该标签后,会自动注册核心 Bean

# @Controller

  • 用于指示该类的实例是一个控制器

# @RequestMapping

  • 可用于类或方法,用来转换 Web 请求(访问路径和参数)
  • 常用属性:
    • value、path:用于将指定请求的实际地址转换到方法上,value 的属性值可以不带斜杠
    • method:用来指定该方法仅仅处理哪些 HTTP 请求方式,包括 GET、POST、HEAD、OPTIONS、PUT、PATCH、DELETE、TRACE,如果没有指定 method 属性值,则请求处理方法可以处理任意的 HTTP 请求方式
    • consumes:指定处理请求的提交内容类型(Content-Type),如 "application/json"、"text/html"、"application/x-www-form-urlencoded"、"multipart/form-data"(MediaType 提供了常用的媒体类型)
    • produces:指定返回的内容类型,返回的内容类型必须是 request 请求头(Accept)中所包含的类型,如 "application/json;charset=UTF-8"、"application/json"
    • headers:指定请求中必须包含某些指定的 header 值,才能让该方法处理,如 "Accept=application/json"
    • params:指定请求中必须包含某些参数值时,才让该方法处理,如 params="myParam=myValue”,方法仅处理其中名为“myParam”、值为“myValue”的请求
  • 组合注解:@GetMapping、@PostMapping、@PutMapping、@DeleteMapping、PatchMapping
  • 后缀匹配:Spring MVC 中默认将 .* 作为匹配后缀,即映射到 /person 的方法也隐式映射到 /person.*。通过重写 WebMvcConfigurerAdapter 类中的 configurePathMatch 方法可设置不忽略“.”后面的参数,configurer.setUseSuffixPatternMatch(false)(Spring Boot 默认设置为 false
  • URI 模式:
    • ? 匹配 1 个字符(但不能是代表路径分隔符的 /
    • * 匹配 0 或多个任意的字符(可以是代表路径分隔符的 /
    • ** 匹配 0 或多个目录
    • {varName:regex},如 {spring:[a-z]+}正则表达式 [a-z]+ 匹配到的值赋值给名为 spring 的路径变量
    • 也可以嵌入 ${…} 占位符,这些占位符在启动时通过 PropertyPlaceHolderConfigurer 对本地、系统、环境和其它属性源来解析
    • 最长匹配原则:存在多个路径匹配模式时,Spring MVC 会以最长符合路径模式来匹配一个路径

# @CrossOrigin

  • 可用于类或方法,设置跨域行为,常用属性:origins(允许域名)、methods、allowedHeaders、exposedHeaders、allowCredentials(是否允许发送 Cookie,启用后允许域名不能设置为 '*')、maxAge(本次预检请求的有效期,单位为秒)
  • Wildcard is not allowed on Access-Control-Allow-Origin when Access-Control-Allow-Credentials is set to true
  • 默认情况下 @CrossOrigin 设置的默认值:允许所有源,允许所有请求头,允许 GET、POST、HEAD 方法,不启用 allowedCredentials,maxAge 被设置为 30 分钟,详见 CorsConfiguration#applyPermitDefaultValues()

# @ResponseStatus

# 参数绑定注解

如果目标方法参数类型不是字符串,则自动应用类型转换

处理方法入参最多只能使用一个 Spring MVC 的注解,否则将抛出异常(即每个参数使用的绑定注解只能有一个)

没有注解的情况下,Spring MVC 也可以获取参数,且参数允许为空,唯一的要求是参数名称和 HTTP 请求的参数名称保持一致

# @RequestParam

  • 用于将指定的请求参数设置到方法参数
  • 属性:name、required(默认 true)、defaultValue

# @PathVariable

  • 用于将 REST 风格的请求 URL 中的动态参数设置到方法参数,属性 value 省略则默认绑定同名参数,默认情况下参数支持简单类型(由 BeanUtils#isSimpleProperty 决定,如 int、long、Date 等)
// 访问 http://localhost/departments/3
@RequestMapping(value = "/departments/{deptId}", method = RequestMethod.DELETE)
@ResponseBody
public void deleteDept(@PathVariable("deptId") Long deptId, HttpServletResponse response) {
    response.setStatus(HttpServletResponse.SC_NO_CONTENT);
}

// 访问 /spring-web-3.0.5.jar,提取名称,版本和文件扩展名
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
public void handle(@PathVariable String name, @PathVariable String version, @PathVariable String ext) {
    // ...
}
1
2
3
4
5
6
7
8
9
10
11
12

# @RequestHeader

  • 用于将请求头中的指定参数值设置到方法参数

# @RequestPart

  • 用于将 multipart/form-data 请求中上传的文件设置到方法参数

# @CookieValue

  • 用于将请求的 Cookie 数据中的指定参数值设置到方法参数

# @ModelAttribute

  • 添加属性到数据模型 Model 中,默认的属性名是首字母小写的属性值类名,属性名可用 @ModelAttribute 中的 value 属性值指定
  • 用法
    1. 修饰方法:Spring MVC 在调用目标处理方法前,会先逐个调用在方法级上标注了 @ModdAttribUte 注解的方法(ModelFactory#invokeModelAttributeMethods),并将这些方法的返回值(null 除外)添加到数据模型中(ModelAttributeMethodProcessor#handleReturnValue)
    2. 修饰方法的参数:将数据模型中的属性值(如果数据模型中不存在则先实例化并到添加到数据模型中)设置到方法参数,再用请求消息填充该方法参数对象(ModelAttributeMethodProcessor#resolveArgument,当参数是非简单类型且 required 为 tue 时,即使方法参数没有使用 @ModelAttribute 修饰,也会进行同样的处理)

# @SessionAttribute

  • 用于将 HttpSession 中的属性值设置到方法参数

# @RequestAttribute

# 信息转换

  • @RestController:修饰类,组合了 @Controller 和 @ResponseBody
  • @RequestBody:修饰参数,用于读取 Request 请求的 body 部分数据,根据 Content-Type 查找匹配的 HttpMessageConverter 进行解析,转换成对应的 Object,并设置到被修饰的方法参数上(application/json、application/xml 等格式的数据必须使用 @RequestBody 来处理)
  • @ResponseBody:可修饰类、方法,将方法返回的对象或集合数据通过适当的消息转换器 HttpMessageConverter 转换为指定格式后,写入到 Response 对象的 body 数据区,并将其返回客户端(此时配置的视图解析器失效)

类使用 @RestController 修饰后,不能再通过返回字符串指定逻辑视图名称,而需要返回 ModelAndView

处理入参的处理器 HandlerMethodArgumentResolver,处理返回值的处理器 HandlerMethodReturnValueHandler,例如两者的实现类:

  1. RequestResponseBodyMethodProcessor 可以处理 @RequestBody(Spring MVC 借助此处理器完成一系列的消息转换器、数据绑定、数据校验等工作)、@ResponseBody 注解
  2. ModelAttributeMethodProcessor 处理 @ModelAttribute 注解的方法参数,并处理 @ModelAttribute 注解的方法的返回值

Spring MVC 在处理 @RequestPart 注解入参数据时,也会执行绑定、校验的相关逻辑,对应处理器是 RequestPartMethodArgumentResolver

# HttpMessageConverter<T> 接口

  • HttpMessageConverter<T> 接口负责通过请求头的 Content-Type 属性将请求体(body)转换为对象(类型为 T),并将对象(类型为 T)绑定到请求方法的参数中,或通过请求头的 Accept 属性将对象写到响应流中

    • 使用 @RequestBody 或 @ResponseBody 对处理方法进行标注
    • 使用 HttpEntity<T> 或 ResponseEntity<T>作为处理方法的入参或返回值
  • 接口中定义的方法

    • boolean canRead(Class<?> clazz, MediaType mediaType):指定转换器可以读取的对象类型,即转换器可将请求信息转换为 clazz 类型的对象,同时指定支持的 MIME 类型
    • boolean canWrite(Class<?> clazz, MediaType mediaType):指定转换器可以将 clazz 类型的对象写到响应流当中,响应流支持的媒体类型在 mediaType 中定义
    • List<MediaType> getSupportedMediaTypes():返回当前转换器支持的媒体类型
    • T read(Class<? extends T> clazz, HttpInputMessage inputMessage):将请求信息流转换为 T 类型的对象
    • void write(T t, MediaType contentType, HttpOutputMessage outputMessage):将 T 类型的对象写到响应流中,同时指定响应的媒体类型为 contentType
  • WebMvcConfigurationSupport#addDefaultHttpMessageConverters 或 RestTemplate 构造器中默认装配的消息转换器

HttpMessageConverter 常见实现类 支持读写的对象类型 支持读取的 MIME 类型 响应的 MIME 类型
ByteArrayHttpMessageConverter byte[] */* application/octet-stream
StringHttpMessageConverter String */* text/plain
ResourceHttpMessageConverter Resource */* application/octet-stream
SourceHttpMessageConverter Source application/xml
text/xml
application/xml
text/xml
FormHttpMessageConverter
AllEncompassingFormHttpMessageConverter
MultiValueMap<String, ?> application/x-www-form-urlencoded application/x-www-form-urlencoded
multipart/form-data
Jaxb2RootElementHttpMessageConverter 使用 @XmlRootElement
或 @XmlType 修饰的类
application/xml
text/xml
application/xml
text/xml
MappingJackson2XmlHttpMessageConverter Object application/xml
text/xml
application/xml
text/xml
MappingJackson2HttpMessageConverter Object application/json application/json

# 转换 JSON 数据

  • JSON 序列化和反序列化转换器,用于转换 Post 请求体中的 JSON 以及将对象序列化为返回响应的 JSON
  • Spring 默认使用 Jackson 处理 json 数据(可通过 HttpMessageConverter 进行自定义配置)
  • Spring MVC 默认使用 MappingJackson2HttpMessageConverter 转换 JSON 格式的数据
  • 在 JSON 和类型化的对象或非类型化的 HashMap 间互相读取和写入
  • 在 Spring Boot 中,可以配置 spring.jackson.date-format=yyyy-MM-dd HH:mm:ss(用于指定序列化和反序列化的时间格式)、 spring.jackson.time-zone=GMT+8,但不支持 Java 8 中新的日期和时间 API
@Configuration
public class JacksonConfig {

    private final static String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
    private final static String DATE_PATTERN = "yyyy-MM-dd";

    /**
     * 注意:自定义 ObjectMapper Bean 时,会导致配置文件中的 spring.jackson.**** 失效
     */
    /*@Bean
    public ObjectMapper objectMapper() {
        return new Jackson2ObjectMapperBuilder().build();
    }*/
    
    // 直接定义 Jackson2ObjectMapperBuilderCustomizer Bean 来实现额外的 ObjectMapper 特性配置
    // 避免自定义 ObjectMapper 覆盖 Spring Boot 自动创建的 ObjectMapper,见 JacksonAutoConfiguration
    // Jackson2ObjectMapperBuilder#customizeDefaultFeatures
    @Bean
    public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(DATE_TIME_PATTERN);
        DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern(DATE_PATTERN);
        return builder -> builder.findModulesViaServiceLoader(true)
                .defaultViewInclusion(false)
                // 遇到未知属性时不抛出 JsonMappingException
                .failOnUnknownProperties(false)
                // .serializationInclusion(JsonInclude.Include.NON_NULL)
                .timeZone(TimeZone.getTimeZone("GMT+8"))
                .simpleDateFormat(DATE_TIME_PATTERN)
                .serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(dateTimeFormatter))
                .deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(dateTimeFormatter))
                .serializerByType(LocalDate.class, new LocalDateSerializer(dateFormatter))
                .deserializerByType(LocalDate.class, new LocalDateDeserializer(dateFormatter));
    }

    // 将自定义反序列化器注册到 Jackson 中
    // Jackson2ObjectMapperBuilderCustomizerConfiguration.StandardJackson2ObjectMapperBuilderCustomizer#configureModules
    @Bean
    public Module enumModule() {
        SimpleModule module = new SimpleModule();
        module.addDeserializer(Enum.class, new EnumDeserializer());
        return module;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
 * 解决序列化反序列化使用枚举中自定义字段的问题,以及找不到枚举值时使用默认值的问题
 */
@Slf4j
public class EnumDeserializer extends JsonDeserializer<Enum> implements ContextualDeserializer {

    private Class<Enum> targetClass;

    public EnumDeserializer() {
    }

    public EnumDeserializer(Class<Enum> targetClass) {
        this.targetClass = targetClass;
    }

    @Override
    public Enum deserialize(JsonParser p, DeserializationContext ctxt) {
        // 找枚举中带有 @JsonValue 注解的字段,这个字段是我们反序列化的基准字段
        Optional<Field> valueFieldOpt = Arrays.stream(targetClass.getDeclaredFields())
                .filter(m -> m.isAnnotationPresent(JsonValue.class))
                .findFirst();

        if (valueFieldOpt.isPresent()) {
            Field valueField = valueFieldOpt.get();
            if (!valueField.isAccessible()) {
                valueField.setAccessible(true);
            }
            // 遍历枚举项,查找字段的值等于反序列化的字符串的那个枚举项
            return Arrays.stream(targetClass.getEnumConstants()).filter(e -> {
                try {
                    return valueField.get(e).toString().equals(p.getValueAsString());
                } catch (Exception ex) {
                    log.error(ex.getMessage(), ex);
                }
                return false;
            }).findFirst().orElseGet(() -> Arrays.stream(targetClass.getEnumConstants()).filter(e -> {
                // 如果找不到,那么就需要寻找默认枚举值来替代,同样遍历所有枚举项,查找 @JsonEnumDefaultValue 注解标识的枚举项
                try {
                    return targetClass.getField(e.name()).isAnnotationPresent(JsonEnumDefaultValue.class);
                } catch (Exception ex) {
                    log.error(ex.getMessage(), ex);
                }
                return false;
            }).findFirst().orElse(null));
        }
        return null;
    }

    @Override
    public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) {
        targetClass = (Class<Enum>) ctxt.getContextualType().getRawClass();
        return new EnumDeserializer(targetClass);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
  • 自定义 JSON 序列化或反序列化转换器
    1. 继承 StdSerializer<T> 或 StdDeserializer<T>,重写相应的抽象方法 serialize() 或 deserialize()
    2. 在相应的字段上添加 @JsonSerialize(using) 或 @JsonDeserialize(using),或者在自定义的序列化或反序列化转换器上添加 @JsonComponent
  • 自定义 Jackson:Jackson2ObjectMapperBuilderCustomizer

# 转换 XML 数据

  • Spring MVC 默认使用 Jaxb2RootElementHttpMessageConverter 转换 XML 格式的数据(当存在 javax.xml.bind.Binder 且不存在 com.fasterxml.jackson.dataformat.xml.XmlMapper 类时)
  • 在 XML(text/xml 或 application/xml)和使用 JAXB2 注解的对象间互相读取和写入
  • JAXB(Java Architecture for XML Binding)是一个业界的标准,是一项可以根据 XML Schema 产生 Java 类的技术。该过程中,JAXB 也提供了将 XML 实例文档反向生成 Java 对象树的方法,并能将 Java 对象树的内容重新写到 XML 实例文档。

# JAXB 常用的注解

  • javax.xml.bind.annotation 中
  • @XmlRootElement:修饰类,将 Java 类或枚举类型转换为 xml 文件中的根节点
  • @XmlAccessorType:修饰类,用于指定由 Java 对象生成 xml 文件时对 Java 对象属性的访问方式,其 value 属性的属性值有 4 个:
    1. XmlAccessType.FIELD:Java 对象中的所有成员变量
    2. XmlAccessType.PROPERTY:Java 对象中所有通过 getter/setter 方式访问的成员变量
    3. XmlAccessType.PUBLIC_MEMBER:Java 对象中所有的 public 访问权限的成员变量和通过 getter/setter 方式访问的成员变量(默认值)
    4. XmlAccessType.NONE:Java 对象的所有属性都不转换为 xml 的元素
  • @XmlAccessorOrder:修饰类,用于对 Java 对象生成的 xml 元素进行排序,其 value 属性的属性值有 2 个:
    • XmlAccessOrder.UNDEFINED:不排序(默认值)
    • AccessorOrder.ALPHABETICAL:对生成的 xml 节点按字母顺序排序
  • @XmlElement:用于把 Java 对象的属性转换为 xml 的子节点,可通过设置其 name 属性值来改变该 java 属性在 xml 文件中的名称
  • @XmlAttribute:用于把 Java 对象的属性转换为 xml 的属性,可通过设置其 name 属性值为生成的 xml 属性指定别名
  • @XmlTransient:用于标示在由 Java 对象转换 xml 时,忽略此属性,即在生成的 xml 文件中不出现此元素
  • @XmlJavaTypeAdapter:常用在转换比较复杂的对象时,如 map 类型或者格式化日期等。使用此注解时,需要写一个 adapter 类继承 XmlAdapter 抽象类,并实现里面的方法,如 @XmlJavaTypeAdapter(value=xxx.class),value 为自己定义的 adapter 类
  • @XmlElementWrapper:对于数组或集合(即包含多个元素的成员变量),生成一个包装该数组或集合的 XML 元素(称为包装器)

# 类型转换

  • 用于转换 RequestParamPathVariable 参数
  • 控制器的参数是处理器通过 Converter、Formatter 和 GenericConverter 这三个接口转换出来的
  • 在 Spring Boot 的初始化中,会将用户自定义的这三个接口的实现类所创建的 Bean 自动注册到 DefaultFormattingConversionService 对象中,由其管理这些转换类(见 WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter#addFormatters)

# Converter<S,T> 接口

  • 一对一的转化器,从一种类型转换为另外一种类型

  • 自定义 Converter 类型 Bean

@Configuration
public class DateFormatConfig {
    @Bean
    public Converter<String, LocalDateTime> localDateTimeConverter() {
        // 不能使用 Lambda 表达式
        // 当使用 Lambda 表达式而不是匿名内部类时,Spring 无法确定泛型类型
        // https://stackoverflow.com/questions/25711858/spring-cant-determine-generic-types-when-lambda-expression-is-used-instead-of-a
        return new Converter<String, LocalDateTime>() {
            @Override
            public LocalDateTime convert(String source) {
                return LocalDateTime.parse(source, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
            }
        };
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# GenericConverter 接口

  • 数组、集合相关转换器
  • 如:StringToArrayConverter、StringToCollectionConverter 分别将逗号分隔的字符串转换为数组、集合;ArrayToCollectionConverter

# Formatter 接口

  • 自定义 Formatter 类型 Bean

# 使用 @ControllerAdvice 配合 @lnitBinder

  • @InitBinder 定义控制器参数绑定规则,如转换规则、格式化等,会在控制器初始化时注册属性编辑器,并在参数转换之前执行
  • WebDataBinder 对象用于处理请求消息和处理方法的绑定工作
@ControllerAdvice
public class TemporalFormatControllerAdvice {
    @InitBinder
    protected void initBinder(WebDataBinder binder) {
        // 绑定验证器
        binder.setValidator(new UserValidator());
        // 注册属性编辑器
        binder.registerCustomEditor(LocalDateTime.class, new PropertyEditorSupport() {
            @Override
            public void setAsText(String text) {
                setValue(LocalDateTime.parse(text, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
            }
        });
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# @DateTimeFormat

  • 可以修饰 Date、Calendar 等时间类型的参数、字段
  • 如 @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss"),该注解支持 Java 8 中新的日期和时间 API
  • 在 Spring Boot 中,可以配置 spring.mvc.date-format=yyyy-MM-dd,但不支持 Java 8 中新的日期和时间 API

# @NumberFormat

# 数据校验

  • Bean Validation (opens new window)
    • Bean Validation 是 Java 定义的一套基于注解/xml 的数据校验规范,包含两部分:Bean Validation API(规范)和 Hibernate Validator (opens new window)(实现)
    • 目前已经从 JSR-303 的 1.0 版本升级到 JSR-349 的 1.1 版本,再到 JSR-380 (opens new window) 的 2.0 版本,已经经历了三个版本
  • jakarta.validation:jakarta.validation-api
  • 通过在 Bean 属性上标注注解指定校验规则,并通过标注的验证接口对 Bean 进行验证
  • 对控制器方法的入参对象进行数据校验,校验结果保存在被校验入参对象之后BindingResult 或 Errors 对象中
    • @Valid:可修饰方法、参数、Bean 属性,对参数进行嵌套验证(递归验证、级联验证,即验证属性关联的对象)时须在相应 Bean 属性/字段/方法参数加上 @Valid
    • @Validated:可修饰类、方法、参数,不可修饰 Bean 属性,但支持分组校验(根据不同的分组采用不同的验证机制)。用于支持 Spring 进行方法级别校验
      • 如果要开启对 Bean 中方法参数或返回值验证,在实现类/接口上标注 @Validated,并创建 MethodValidationPostProcessor Bean
      • 在校验方法入参时,校验注解应标注在父类/接口的方法参数上,而不是子类/实现类的方法参数上
      • 校验错误后抛出 ConstraintViolationException
  • 如果控制器方法的参数列表中没有 BindingResult 或 Errors,默认情况下,校验错误后抛出 MethodArgumentNotValidException 并被转换为 400(BAD_REQUEST)响应
  1. 使用 form data 方式调用接口,校验异常抛出 BindException
  2. 使用 json 请求体调用接口,校验异常抛出 MethodArgumentNotValidException
  3. 单个参数校验异常抛出 ConstraintViolationException
  • 自定义校验注解:implements javax.validation.ConstraintValidator<A extends Annotation, T>

  • 自定义校验消息:在 src/main/resources/ 目录下创建 ValidationMessages.properties、ValidationMessages_zh_CN.properties

  • 自定义验证器:implements Validator,并通过 WebDataBinder.setValidator 绑定

  • JSR-303 注解 JSR-303注解

  • Hibernate Validator 扩展的注解 HibernateValidator 扩展的注解

# URI 链接

  • 构造 URI:UriComponentsBuilder

    // https://example.com/hotel%20list/New%20York?q=foo%2Bbar
    URI uri = UriComponentsBuilder.fromHttpUrl("https://example.com/hotel list/{hotel}")
        .queryParam("q", "{q}")
        .encode() // 编码,UriComponentsBuilder#encode()
        .buildAndExpand("New York", "foo+bar") // 扩展 URI 变量,返回 UriComponents
        .toUri(); // 获取 URI
    
    URI uri = UriComponentsBuilder.fromHttpUrl("https://example.com/hotel list/{hotel}")
        .queryParam("q", "{q}")
        .build("New York", "foo+bar");
    
    URI uri = UriComponentsBuilder.fromHttpUrl("https://example.com/hotel list/{hotel}?q={q}")
        .build("New York", "foo+bar");
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
  • 构造相对 Servlet 请求的 URI:ServletUriComponentsBuilder

    // 构造相对于当前请求的 URI
    URI uri = ServletUriComponentsBuilder.fromRequest(request)
            .replaceQueryParam("accountId", "{id}").build()
            .expand("123")
            .encode() // UriComponents#encode()
            .toUri(); 
    // 构造相对于上下文路径的 URI
    URI uri = ServletUriComponentsBuilder.fromContextPath(request)
            .path("/accounts").build().toUri();
    // 构造相对于 Servlet 的 URI
    URI uri = ServletUriComponentsBuilder.fromServletMapping(request)
        .path("/accounts").build().toUri();
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
  • 构造指向 Controller 的 URI :MvcUriComponentsBuilder

    URI uri = MvcUriComponentsBuilder
        .fromMethodName(BookingController.class, "getBooking", 21).buildAndExpand(42)
        .encode().toUri();
    URI uri = MvcUriComponentsBuilder.fromMethodCall(on(BookingController.class)
            .getBooking(21)).buildAndExpand(42).encode().toUri();
    
    @Controller
    @RequestMapping("/hotels/{hotel}")
    public class BookingController {
    
        @GetMapping("/bookings/{booking}")
        public String getBooking(@PathVariable Long booking) {
            // ...
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
  • URI 编码

    1. UriComponentsBuilder#encode():首先对 URI 模板进行预编码,然后在扩展时严格对 URI 变量进行编码,即还会替换出现在 URI 变量中的具有保留含义的字符,如 URI 变量中的 ":" 替换为 "%3A"
    2. UriComponents#encode():扩展 URI 变量后,对 URI 组件进行编码(不会替换出现在 URI 变量中的具有保留含义的字符)
    • 编码规则如下:在 URI 组件中,将百分比编码应用到所有非法字符,包括 non-US-ASCII 字符,以及在 RFC 3986 (opens new window) 中定义的 URI 组件内的特殊字符,即将需要转换的内容使用 UTF-8 编码后,再使用十六进制表示法转换,并在之前加上 % 开头

    • URI 语法 (opens new window)scheme:[//[user:password@]host[:port]][/]path[?query][#fragment]

      • URI = scheme:[//authority]path[?query][#fragment]
      • authority = [userinfo@]host[:port]

      URI_syntax_diagram

# 文件上传

  • 在 XML 中配置文件上传解析器 MultipartResolver(默认没有装配),实现类:StandardServletMultipartResolver、CommonsMultipartResolver(需要依赖 commons-fileupload:commons-fileupload)

    <!-- 配置文件上传解析器,其 id 固定 -->
    <bean id="multipartResolver"
        class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <!-- 上传文件大小上限,单位为字节(3 MB)-->
        <property name="maxUploadSize" value="#{3*1024*1024}"/>
    </bean>
    
    1
    2
    3
    4
    5
    6
  • 在调用控制器之前 DispatcherServlet 会将 HttpServletRequest 转换为 MultipartHttpServletRequest 对象(StandardMultipartHttpServletRequest#parseRequest)

  • 在 Controller 方法中(可在 @PostMapping 中指定 consumes = MediaType.MULTIPART_FORM_DATA_VALUE),通过 MultipartFile file 来接收上传的文件,通过 MultipartFile[] files 接收多个文件上传,或者使用 javax.servlet.http.Part 接口

  • MultipartFile 接口
    boolean isEmpty():是否有上传的文件
    String getName():获取表单中文件上传组件的名字
    String getContentType():获取文件 MIME 类型,如 image/jpeg 等
    String getOriginalFilename():获取上传文件的原名
    long getSize():获取文件的字节大小,单位为 byte
    InputStream getInputStream():获取文件流
    void transferTo(File dest):将上传文件保存到一个目标文件中

# 文件下载

  • new ResponseEntity(T body, HttpHeaders headers, HttpStatus status)
// http://localhost/files/first.txt
@GetMapping("/files/{filename:.+}")
@ResponseBody
public ResponseEntity<Resource> serveFile(@PathVariable String filename) {
    Resource file = ...;
    return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getFilename() + "\"").body(file);
}
1
2
3
4
5
6
7

# 过滤器

Spring MVC 中的过滤器:

  1. CompositeFilter:implements Filter
  2. 抽象类 GenericFilterBean:
    • implements Filter, BeanNameAware, EnvironmentAware, EnvironmentCapable, ServletContextAware, InitializingBean, DisposableBean
    • 子类:OncePerRequestFilter、AbstractRequestLoggingFilter、DelegatingFilterProxy
  • 常见过滤器:CharacterEncodingFilter、RequestContextFilter、FormContentFilter、HiddenHttpMethodFilter、ShallowEtagHeaderFilter、CorsWebFilter

# 拦截器

  • 自定义拦截器继承 HandlerInterceptorAdapter 抽象类

  • 重写拦截方法

    1. boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler):该方法将在请求处理之前被调用,当返回值为 false 时,表示表示拦截,请求结束;当返回值为 true 时,表示放行
    2. void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView):处理器处理后方法
    3. void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
  • 在 mvc.xml 文件中配置拦截器

    <mvc:interceptors>
        <mvc:interceptor>
            <!-- /* 只能拦截一级路径;/** 可以拦截一级或多级路径 -->
            <mvc:mapping path="/**"/>
            <mvc:exclude-mapping path="/login.do"/>
            <bean class="自定义拦截器的类名"/>
        </mvc:interceptor>
    </mvc:interceptors>
    
    1
    2
    3
    4
    5
    6
    7
    8

过滤器和拦截器的执行顺序

# 请求前后增强处理

  • RequestBodyAdvice,用于对带有 @RequestBody 注解的 Controller 方法,在读取请求 body 之前或者在 body 转换成对象之前做相应的增强(如消息体解密、日志记录)
  • ResponseBodyAdvice,用于对使用 @ResponseBody 修饰的 Controller 方法,在响应体写出之前做相应的增强
  • 需加上 @ControllerAdvice 注解
@Slf4j
@ControllerAdvice
public class LogRequestBodyAdvice implements RequestBodyAdvice {
    @Override
    public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }

    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> converterType) {
        return httpInputMessage;
    }

    @Override
    public Object afterBodyRead(Object body, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> converterType) {
        Method method = methodParameter.getMethod();
        log.info("{}.{}:{}", method.getDeclaringClass().getSimpleName(), method.getName(), JSON.toJSONString(body));
        return body;
    }

    @Override
    public Object handleEmptyBody(Object body, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> converterType) {
        Method method = methodParameter.getMethod();
        log.info("{}.{}", method.getDeclaringClass().getSimpleName(), method.getName());
        return body;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Slf4j
@ControllerAdvice
public class LogResponseBodyAdvice implements ResponseBodyAdvice {
    @Override
    public boolean supports(MethodParameter methodParameter, Class aClass) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        Method method = methodParameter.getMethod();
        String url = serverHttpRequest.getURI().toASCIIString();
        log.info("{}.{}, url={}, result={}", method.getDeclaringClass().getSimpleName(), method.getName(), url, JSON.toJSONString(body));
        return body;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 异常统一处理

WebApplicationContext 中声明的 HandlerExceptionResolver bean 用来解析处理请求时抛出的异常,其实现类:SimpleMappingExceptionResolver、DefaultHandlerExceptionResolver、ResponseStatusExceptionResolver、ExceptionHandlerExceptionResolver

# 使用 XML 文件配置

<!-- 配置简单异常处理器 -->
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
    <!-- 定义出现异常时默认跳转的视图 -->
    <property name="defaultErrorView" value="common/error"/>
    <!-- 定义异常的变量名,默认名为 exception -->
    <property name="exceptionAttribute" value="ex"/>
    <!-- 定义需要特殊处理的异常,用类名或完全路径名作为 key,异常跳转视图作为值 -->
    <property name="exceptionMappings">
        <value>
            com.example.wms.exception.SecurityException=common/nopermission
            <!-- 这里还可以继续扩展不同异常类型的异常处理 -->
        </value>
    </property>
</bean>
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 使用统一的异常处理类

  • 使用 @ControllerAdvice 或 @RestControllerAdvice 定义统一的异常处理类,用来拦截 Controller 的方法
  • 使用 @ExceptionHandler(value = Exception.class) 指定该方法处理的异常类型
  • 使用 @ResponseStatus(HttpStatus.xxx) 指定该方法返回的状态码
/**
 * SpringMVC 自定义异常对应的 status code
 *            Exception                       HTTP Status Code
 * ConversionNotSupportedException         500 (Internal Server Error)
 * HttpMessageNotWritableException         500 (Internal Server Error)
 * HttpMediaTypeNotSupportedException      415 (Unsupported Media Type)
 * HttpMediaTypeNotAcceptableException     406 (Not Acceptable)
 * HttpRequestMethodNotSupportedException  405 (Method Not Allowed)
 * NoSuchRequestHandlingMethodException    404 (Not Found)
 * TypeMismatchException                   400 (Bad Request)
 * HttpMessageNotReadableException         400 (Bad Request)
 * MissingServletRequestParameterException 400 (Bad Request)
*/

@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(AuthorizationException.class)
    @ResponseStatus(HttpStatus.FORBIDDEN)
    public void handlerAuthorizationException(HandlerMethod method, HttpServletResponse response, AuthorizationException exception) throws Exception {
        if (method.getMethod().isAnnotationPresent(ResponseBody.class)) {
            // ajax 请求
            response.setCharacterEncoding("utf-8");
            response.getWriter().print(new ObjectMapper().writeValueAsString(new JsonResult().mark(("没有权限!"))));
        } else {
            // 资源访问
            response.sendRedirect("/nopermission.jsp");
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 统一接口版本

在 Controller 上或接口方法上使用 @APIVersion("v1"),来实现以统一的 Pattern 进行版本号控制

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface APIVersion {
    String[] value();
}
1
2
3
4
5
public class APIVersionHandlerMapping extends RequestMappingHandlerMapping {

    @Override
    protected boolean isHandler(Class<?> beanType) {
        return AnnotatedElementUtils.hasAnnotation(beanType, Controller.class);
    }

    @Override
    protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping) {
        Class<?> controllerClass = method.getDeclaringClass();
        APIVersion apiVersion = AnnotationUtils.findAnnotation(method, APIVersion.class);
        if (apiVersion == null) {
            apiVersion = AnnotationUtils.findAnnotation(controllerClass, APIVersion.class);
        }

        String[] urlPatterns = apiVersion == null ? new String[0] : apiVersion.value();

        PatternsRequestCondition apiPattern = new PatternsRequestCondition(urlPatterns);
        PatternsRequestCondition oldPattern = mapping.getPatternsCondition();
        PatternsRequestCondition updatedFinalPattern = apiPattern.combine(oldPattern);
        mapping = new RequestMappingInfo(mapping.getName(), updatedFinalPattern, mapping.getMethodsCondition(),
                mapping.getParamsCondition(), mapping.getHeadersCondition(), mapping.getConsumesCondition(),
                mapping.getProducesCondition(), mapping.getCustomCondition());
        super.registerHandlerMethod(handler, method, mapping);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

最后,定义 Bean 实现 WebMvcRegistrations 接口,重写 getRequestMappingHandlerMapping() 方法来使 APIVersionHandlerMapping 生效

# 工具类

# HttpServletRequest、HttpServletResponse 的包装类

  • HttpServletRequestWrapper、HttpServletResponseWrapper

  • ContentCachingRequestWrapper:HttpServletRequest 包装器,用于缓存从 request.getInputStream()request.getReader() 读取的内容,并允许通过 getContentAsByteArray() 取出内容

  • ContentCachingResponseWrapper:HttpServletResponse 包装器,用于缓存从 request.getInputStream()request.getReader() 读取的内容,并允许通过 getContentAsByteArray() 取出内容

// 需要在 doFilter 前调用
ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response);
1
2
3

读取完 body 之后,需要调用 wrappedResponse.copyBodyToResponse(); 将缓存的 body 复制到响应中,使得客户端可以正常接收响应

# WebUtils

  • T getNativeRequest(ServletRequest request, Class<T> requiredType):返回指定类型的合适的请求对象
  • T getNativeResponse(ServletResponse response, Class<T> requiredType):返回指定类型的合适的响应对象
  • File getTempDir(ServletContext servletContext):返回由当前 servlet 容器提供的,当前 Web 应用程序的临时目录
  • String getRealPath(ServletContext servletContext, String path):返回由 servlet 容器提供的,Web 应用程序中给定路径的实际路径
  • String getSessionId(HttpServletRequest request):如果没有 session id 或没有 session 返回 null
  • Object getSessionAttribute(HttpServletRequest request, String name):通过 name 获取 session 中的属性,如果 session 中没有该属性或者没有 session 返回 null
  • Object getRequiredSessionAttribute(HttpServletRequest request, String name):通过 name 获取 session 中的属性,如果 session 中没有该属性或者没有 session 则抛出异常
  • void setSessionAttribute(HttpServletRequest request, String name, Object value):通过给定的名称和值设置 session 中的属性,若果值为 null,则移除 session 在的属性
  • Cookie getCookie(HttpServletRequest request, String name):获取具有给定名称的第一个 cookie
  • String findParameterValue(ServletRequest request, String name):获取具有给定参数名称的第一个值
  • MultiValueMap<String, String> parseMatrixVariables(String matrixVariables):用矩阵变量解析给定的字符串

# UriUtils

  • 基于 RFC 3986 的 URI 编码和解码实用方法
  • String encode(String source, Charset charset)
  • String encodePath(String path, String encoding)
  • String encodePathSegment(String segment, Charset charset)
  • Map<String, String> encodeUriVariables(Map<String, ?> uriVariables)
  • String encodeQueryParam(String queryParam, Charset charset)
  • MultiValueMap<String, String> encodeQueryParams(MultiValueMap<String, String> params)
  • String decode(String source, Charset charset)
Updated at: 2024-04-22 15:04:27